﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;

namespace DataService.Helper.Tail
{
    public class TailCommandLine 
    {
        #region Fields

        private TailCommandLineOptions options;
        private System.Threading.Timer monitoringFileTimer = null;
        private object monitoringFileLocker = new object();
        private volatile bool isMonitoring = false;
        private FileSystemWatcher fileSystemWatcher = null;
        private long previousSeekPosition = 0;
        private int maxReadBytes = 1024 * 16;
        private long readBytesCount = 0;
        private Encoding encoding = Encoding.ASCII;
        private byte[] newLine = Encoding.ASCII.GetBytes("\n");
        private long readLineCount = 0;
        private AutoResetEvent signal = new AutoResetEvent(false);

        #endregion

        #region ICommandLine Members

        public void Execute(string filepath)
        {
            options = new TailCommandLineOptions();
            options.File = filepath;
            options.IsSetFollow = true;
            options.IsSetRetry = true;
            StartWatch();
        }

        public void Terminate()
        {
            StopWatch();
        }

        #endregion

        #region Private Methods

        [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        private void StartWatch()
        {
            try
            {
                FileInfo targetFile = new FileInfo(options.File);

                previousSeekPosition = 0;

                fileSystemWatcher = new FileSystemWatcher();
                fileSystemWatcher.IncludeSubdirectories = false;
                fileSystemWatcher.Path = targetFile.DirectoryName;
                fileSystemWatcher.Filter = targetFile.Name;

                fileSystemWatcher.Created += new FileSystemEventHandler(OnFileCreated);
                fileSystemWatcher.Deleted += new FileSystemEventHandler(OnFileDeleted);
                fileSystemWatcher.Renamed += new RenamedEventHandler(OnFileRenamed);
                fileSystemWatcher.Changed += new FileSystemEventHandler(OnFileChanged);
                fileSystemWatcher.EnableRaisingEvents = true;

                if (targetFile.Exists)
                {
                    while (targetFile.Length > readBytesCount && readLineCount < options.OutputLines)
                    {
                        ReadFile();
                    }
                }
                else
                {
                    if (!options.IsSetRetry)
                    {
                        //
                    }
                }

                if (options.IsSetFollow)
                {
                    if (monitoringFileTimer == null)
                    {
                        monitoringFileTimer = new System.Threading.Timer((TimerCallback)OnMonitoringTimerCallback, options.File,
                            TimeSpan.Zero, TimeSpan.FromMilliseconds(options.SleepInterval));
                    }
                }
                else
                {
                    Terminate();
                }
            }
            catch (Exception ex)
            {
                //
            }
        }

        [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        private void StopWatch()
        {
            signal.Set();

            if (fileSystemWatcher != null)
            {
                fileSystemWatcher.Created -= new FileSystemEventHandler(OnFileCreated);
                fileSystemWatcher.Deleted -= new FileSystemEventHandler(OnFileDeleted);
                fileSystemWatcher.Renamed -= new RenamedEventHandler(OnFileRenamed);
                fileSystemWatcher.Changed -= new FileSystemEventHandler(OnFileChanged);
                fileSystemWatcher.EnableRaisingEvents = false;
                fileSystemWatcher.Dispose();
                fileSystemWatcher = null;
            }

            if (monitoringFileTimer != null)
            {
                monitoringFileTimer.Dispose();
                monitoringFileTimer = null;
            }
        }

        private void RestartWatch()
        {
            StopWatch();
            StartWatch();
        }

        private void ReadFile()
        {
            if (this.previousSeekPosition == 0)
            {
                ReadFirstTime();
            }
            else
            {
                ReadContinue();
            }
        }

        private void ReadFirstTime()
        {
            byte[] readBytes = new byte[maxReadBytes];
            int numReadBytes = 0;

            this.previousSeekPosition = 0;
            using (FileStream fs = new FileStream(options.File, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                if (fs.Length > 0)
                {
                    CheckFileEncoding(fs);
                }

                this.previousSeekPosition = 0;
                if (fs.Length > maxReadBytes)
                {
                    this.previousSeekPosition = fs.Length - maxReadBytes;
                }

                this.previousSeekPosition = fs.Seek(this.previousSeekPosition, SeekOrigin.Begin);
                numReadBytes = fs.Read(readBytes, 0, (int)maxReadBytes);
                this.previousSeekPosition += numReadBytes;
            }

            if (numReadBytes > 0)
            {
                byte[] newLineBuffer = new byte[newLine.Length];
                byte[] reverseBytes = new byte[maxReadBytes];
                int reverseCount = 0;
                bool isNewLineEqual = true;

                for (int i = numReadBytes - 1; i >= 0; i--)
                {
                    Buffer.BlockCopy(readBytes, i, reverseBytes, reverseCount, 1);
                    reverseCount++;
                    readBytesCount++;

                    if (newLineBuffer.Length == 1)
                    {
                        Buffer.BlockCopy(readBytes, i, newLineBuffer, 0, newLineBuffer.Length);
                    }
                    else if (newLineBuffer.Length > 1 && numReadBytes - i >= newLineBuffer.Length)
                    {
                        Buffer.BlockCopy(readBytes, i, newLineBuffer, 0, newLineBuffer.Length);
                    }
                    isNewLineEqual = true;
                    for (int b = 0; b < newLine.Length; b++)
                    {
                        if (newLineBuffer[b] != newLine[b])
                        {
                            isNewLineEqual = false;
                            break;
                        }
                    }
                    if (isNewLineEqual)
                    {
                        readLineCount++;
                        if (readLineCount - 1 >= options.OutputLines)
                        {
                            break;
                        }
                    }
                }

                if (reverseCount > 0)
                {
                    byte[] transferBuffer = new byte[reverseCount];
                    int transferIndex = 0;
                    for (int i = reverseCount - 1; i >= 0; i--)
                    {
                        Buffer.BlockCopy(reverseBytes, i, transferBuffer, transferIndex, 1);
                        transferIndex++;
                    }

                    string data = encoding.GetString(transferBuffer, 0, reverseCount);
                    //CommandLineDataChanged(this, new CommandLineDataChangedEventArgs(data));
                }
            }
        }

        private void ReadContinue()
        {
            try
            {
                byte[] readBytes = new byte[maxReadBytes];
                int numReadBytes = 0;

                using (FileStream fs = new FileStream(options.File, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    if (fs.Length > this.previousSeekPosition)
                    {
                        long willReadCount = 0;
                        if (fs.Length - this.previousSeekPosition >= maxReadBytes)
                        {
                            willReadCount = maxReadBytes;
                        }
                        else
                        {
                            willReadCount = fs.Length - this.previousSeekPosition;
                        }

                        this.previousSeekPosition = fs.Seek(this.previousSeekPosition, SeekOrigin.Begin);
                        numReadBytes = fs.Read(readBytes, 0, (int)willReadCount);
                        this.previousSeekPosition += numReadBytes;
                    }
                    else if (fs.Length < this.previousSeekPosition)
                    {
                        this.previousSeekPosition = 0;
                    }
                }

                if (numReadBytes > 0)
                {
                    byte[] newLineBuffer = new byte[newLine.Length];
                    bool isNewLineEqual = true;

                    for (int i = 0; i < numReadBytes; i++)
                    {
                        if (newLineBuffer.Length == 1)
                        {
                            Buffer.BlockCopy(readBytes, i, newLineBuffer, 0, newLineBuffer.Length);
                        }
                        else if (newLineBuffer.Length > 1)
                        {
                            if (numReadBytes - i >= newLineBuffer.Length)
                            {
                                Buffer.BlockCopy(readBytes, i, newLineBuffer, 0, newLineBuffer.Length);
                            }
                            else
                            {
                                Buffer.BlockCopy(readBytes, i, newLineBuffer, 0, 1);
                            }
                        }
                        isNewLineEqual = true;
                        for (int b = 0; b < newLine.Length; b++)
                        {
                            if (newLineBuffer[b] != newLine[b])
                            {
                                isNewLineEqual = false;
                                break;
                            }
                        }
                        if (isNewLineEqual)
                        {
                            readLineCount++;
                        }

                        readBytesCount++;
                    }

                    string data = encoding.GetString(readBytes, 0, numReadBytes);
                    CommandLineDataChanged(this, new CommandLineDataChangedEventArgs(data));
                }
            }
            catch (Exception e)
            {

                
            }
        }
        public event EventHandler<CommandLineDataChangedEventArgs> CommandLineDataChanged;

        private void CheckFileEncoding(FileStream fs)
        {
            byte[] checkEncodingBuffer = new byte[3];

            fs.Read(checkEncodingBuffer, 0, 3);

            if (checkEncodingBuffer[0] == 0xEF
              && checkEncodingBuffer[1] == 0xBB
              && checkEncodingBuffer[2] == 0xBF)
            {
                // UTF-8 : EF BB BF
                encoding = Encoding.UTF8;
                newLine = encoding.GetBytes(Environment.NewLine);
            }
            else if (checkEncodingBuffer[0] == 0xFF
              && checkEncodingBuffer[1] == 0xFE)
            {
                // Unicode Little Endian : FF FE
                encoding = Encoding.Unicode;
                newLine = encoding.GetBytes(Environment.NewLine);
            }
            else if (checkEncodingBuffer[0] == 0xFE
              && checkEncodingBuffer[1] == 0xFF)
            {
                // Unicode Big Endian : FE FF
                encoding = Encoding.BigEndianUnicode;
                newLine = encoding.GetBytes(Environment.NewLine);
            }
        }

        private void OnMonitoringTimerCallback(object state)
        {
            if (!isMonitoring)
            {
                lock (monitoringFileLocker)
                {
                    isMonitoring = true;

                    FileInfo targetFile = new FileInfo(state.ToString());
                    if (targetFile.Exists)
                    {
                        if (options.IsSetFollow)
                        {
                            if (targetFile.Length > this.previousSeekPosition)
                            {
                                if (targetFile.Name.Contains("PE"))
                                {
                                    if (targetFile.Length - this.previousSeekPosition > 95)
                                    {
                                        ReadFile();
                                    }
                                }
                                else
                                {
                                    ReadFile();
                                }
                                
                            }
                        }
                        else
                        {
                            if (targetFile.Length > readBytesCount && readLineCount < options.OutputLines)
                            {
                                ReadFile();
                            }
                        }
                    }
                    isMonitoring = false;
                }
            }
        }

        private void OnFileCreated(object source, FileSystemEventArgs e)
        {
        }

        private void OnFileChanged(object sender, FileSystemEventArgs e)
        {
        }

        private void OnFileRenamed(object sender, RenamedEventArgs e)
        {
            RestartWatch();
        }

        private void OnFileDeleted(object sender, FileSystemEventArgs e)
        {
            RestartWatch();
        }

        #endregion
    }
}
